{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 量子生成对抗网络"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<em> Copyright (c) 2021 Institute for Quantum Computing, Baidu Inc. All Rights Reserved. </em>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 经典生成对抗网络"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 生成对抗网络简介"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "生成对抗网络（generative adversarial network, GAN）是生成模型的一种，是深度学习在近些年中一个重要的发展[1]。它分为两个部分：生成器 $G$（generator）和判别器 $D$ (discriminator)。生成器接受随机的噪声信号，以此为输入来生成我们期望得到的数据。判别器判断接收到的数据是不是来自真实数据，通常输出一个 $P(x)$，表示输入数据 $x$ 是真实数据的概率。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 纳什均衡"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "在这里，我们用纳什均衡的思想来探讨 GAN 的收敛问题。\n",
    "\n",
    "纳什均衡（Nash equilibrium）是指在包含两个或以上参与者的非合作博弈（non-cooperative game）中，假设每个参与者都知道其他参与者的均衡策略的情况下，没有参与者可以通过改变自身策略使自身受益时的一个概念解。在博弈论中，如果每个参与者都选择了自己的策略，并且没有玩家可以通过改变策略而其他参与者保持不变而获益，那么当前的策略选择的集合及其相应的结果构成了纳什均衡。\n",
    "\n",
    "我们可以把GAN的训练过程视为生成器和判别器的博弈过程。在这个博弈过程中，无论生成器的策略是什么，判别器最好的策略就是尽量判别出真实数据和生成数据。而无论判别器的策略是什么，生成器最好的策略就是使判别器无法判别出来。我们不难发现，这种博弈是零和博弈（一种非合作博弈），即一方有所得则另一方必有所失。因此生成器和判别器的博弈存在这种纳什均衡策略。而当真实数据的样本足够多，双方的学习能力足够强时，最终就会达到一种纳什均衡点。**生成器具备了生成真实数据的能力，而判别器也无法再区分生成数据和真实数据。**"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 优化目标"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "在 GAN 中，我们重点想要得到的是一个优秀的生成器（但是只有优秀的判别器才能准确判断生成器是否优秀），所以我们训练的理想结果是判别器无法识别出数据是来自真实数据还是生成数据。\n",
    "\n",
    "因此我们的目标函数如下：\n",
    "\n",
    "$$\n",
    "\\min_{G}\\max_{D} V(G,D)= \\min_{G}\\max_{D}\\mathbb{E}_{x\\sim P_{data}}[\\log D(x)]+\\mathbb{E}_{z\\sim P_{z}}[\\log(1-D(G(z)))]. \\tag{1}\n",
    "$$\n",
    "\n",
    "这里，$G$ 表示生成器的参数，$D$ 表示判别器的参数。实际过程中，通常采用交替训练的方式，即先固定 $G$，训练 $D$，然后再固定 $D$，训练 $G$，不断往复。当两者的性能足够时，模型会收敛，两者达到纳什均衡。\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 优点"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "- 相对其他生成模型，GAN 的生成效果更好。\n",
    "- 理论上，只要是可微分函数都可以用于构建生成器和判别器，因此能够与深度神经网络结合做深度生成模型。\n",
    "- GAN 相对其他生成模型来说，不依赖先验假设，我们事先不需要假设数据的分布和规律。\n",
    "- GAN 生成数据的形式也很简单，只需要通过生成器进行前向传播即可。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 缺点"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "- GAN 无需预先建模，因此过于自由导致训练难以收敛而且不稳定。\n",
    "- GAN 存在梯度消失问题，即很可能会达到这样一种状态，判别器的效果特别好，生成器的效果特别差。在这种情况下，判别器的训练没有任何损失，因此也没有有效的梯度信息去回传给生成器让它优化自己。\n",
    "- GAN 的学习过程可能出现模式崩溃（model collapse）问题。生成器发生退化，总是生成同样的样本点，无法继续学习。而此时，判别器也会对相似的样本点指向相似的方向，模型参数已经不再更新，但是实际效果却很差。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 量子生成对抗网络"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "量子生成对抗网络与经典的类似，只不过不再用于生成经典数据，而是生成量子态[2-3]。在实践中，如果我们有一个量子态，其在观测后会坍缩为某一本征态，无法恢复到之前的量子态。因此如果我们有一个方法可以根据已有的目标量子态生成出很多与之相同（或相近）的量子态，会很方便我们的实验。\n",
    "\n",
    "假设我们已有的目标量子态是一个混合态，它们属于同一个系综，其密度算符为$\\rho$。然后我们需要有一个生成器 $G$，它的输入是一个噪声数据，我们用一个系综 $\\rho_{z}=\\sum_{i}p_{i}|z_{i}\\rangle\\langle z_{i}|$ 来表示。因此我们每次取出一个随机噪声样本 $|z_{i}\\rangle$，通过生成器后得到生成的量子态 $|x\\rangle=G|z_{i}\\rangle$，我们期望生成的 $|x\\rangle$ 与目标量子态$\\rho$相近。\n",
    "\n",
    "值得注意的是，对于上文中提到的目标态的系综和噪声数据的系综，我们都认为有一个已有的物理设备可以生成出一个该系综下的量子态，而由于量子物理的相关性质，我们每次可以得到一个真正随机的量子态。但是在计算机程序中，我们仍然只能模拟这一过程。\n",
    "\n",
    "对于判别器，我们期望判别器可以判断我们输入的量子态是已有的目标态还是生成的量子态，这一过程可以由测量给出。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 一个简单的例子"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 问题描述"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "简单起见，我们假设已有的目标量子态是一个纯态，且生成器接受的输入为$|0\\rangle$。\n",
    "\n",
    "制备已有的目标量子态的线路：\n",
    "![QGAN-fig-target_state](figures/QGAN-fig-target_state.png)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "生成器的线路为：\n",
    "![QGAN-fig-generator](figures/QGAN-fig-generator.png)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "判别器的线路为：\n",
    "![QGAN-fig-discriminator](figures/QGAN-fig-discriminator.png)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "通过对判别器输出的量子态进行测量，我们可以得到将目标态判断为目标态的概率 $P_{T}$ 和将生成态判断为目标态的概率 $P_{G}$（通过对判别器连接目标态和生成器这两个不同的输入得到）。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 具体过程"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "假设已有的目标量子态为 $|\\psi\\rangle$，生成器生成的量子态为 $|x\\rangle=G|00\\rangle$（生成器采用两量子比特线路，其中第0个量子比特认为是生成的量子态）。\n",
    "\n",
    "判别器对数据进行判别并得到量子态$|\\phi\\rangle$，那么当输入为目标态时，$|\\phi\\rangle=D(|\\psi\\rangle\\otimes |00\\rangle)$；当输入为生成态时，$|\\phi\\rangle=D(G\\otimes I)|000\\rangle$。\n",
    "\n",
    "对于判别器得到的量子态，我们还需要采用泡利 Z 门对第3个量子比特进行测量，从而得到判别器对输入量子态的判断结果（即判别器认为输入是目标态的概率）。首先有 $M_{z}=I\\otimes I\\otimes\\sigma_{z}$，而测量结果为 $\\text{disc_output}=\\langle\\phi|M_{z}|\\phi\\rangle$，所以测量结果为目标态的概率是 $P=(\\text{disc_output}+1)/2$。\n",
    "\n",
    "我们定义判别器的损失函数为 $\\mathcal{L}_D=P_{G}(\\text{gen_theta}, \\text{disc_phi})-P_{T}(\\text{disc_phi})$，生成器的损失函数为 $\\mathcal{L}_{G}=-P_{G}(\\text{gen_theta}, \\text{disc_phi})$。这里的 $P_{G}$ 和 $P_{T}$ 分别是输入量子态为生成态和目标态时，$P=(\\text{disc_output}+1)/2$ 的表达式，gen\\_theta 和 disc\\_phi 分别是生成器和判别器线路的参数。\n",
    "\n",
    "因此我们只需要分别优化目标函数 $\\min_{\\text{disc_phi}}\\mathcal{L}_{D}$ 和 $\\min_{\\text{gen_theta}}\\mathcal{L}_{G}$ 即可交替训练判别器和生成器。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 在 Paddle Quantum 上的实现"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "首先导入相关的包。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "import scipy\n",
    "import warnings\n",
    "import paddle\n",
    "import paddle_quantum\n",
    "from paddle_quantum.state import zero_state\n",
    "from paddle_quantum.ansatz import Circuit\n",
    "from paddle_quantum import Hamiltonian\n",
    "from paddle_quantum.loss import ExpecVal\n",
    "from paddle_quantum.qinfo import partial_trace\n",
    "from tqdm import tqdm\n",
    "\n",
    "warnings.filterwarnings(\"ignore\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "然后定义我们的网络模型 QGAN。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 设置模拟方式为密度矩阵\n",
    "paddle_quantum.set_backend('density_matrix')\n",
    "\n",
    "\n",
    "class QGAN(paddle_quantum.gate.Gate):\n",
    "    def __init__(self, target_state):\n",
    "        super().__init__()\n",
    "        self.target_state = target_state.clone()\n",
    "\n",
    "        # 生成器的量子电路\n",
    "        self.generator = Circuit(3)\n",
    "        self.generator.u3([0, 1])\n",
    "        self.generator.cnot([0, 1])\n",
    "        self.generator.u3(0)\n",
    "\n",
    "        # 判别器的量子电路\n",
    "        self.discriminator = Circuit(3)\n",
    "        self.discriminator.u3([0, 2])\n",
    "        self.discriminator.cnot([0, 2])\n",
    "        self.discriminator.u3(0)\n",
    "\n",
    "    def disc_target_as_target(self):\n",
    "        \"\"\"\n",
    "        判别器将目标态判断为目标态的概率\n",
    "        \"\"\"\n",
    "        state = self.discriminator(self.target_state)\n",
    "        expec_val_func = ExpecVal(Hamiltonian([[1.0, 'z2']]))\n",
    "        \n",
    "        # 判别器对目标态的判断结果\n",
    "        target_disc_output = expec_val_func(state)\n",
    "        prob_as_target = (target_disc_output + 1) / 2\n",
    "\n",
    "        return prob_as_target\n",
    "\n",
    "    def disc_gen_as_target(self):\n",
    "        \"\"\"\n",
    "        判别器将生成态判断为目标态的概率\n",
    "        \"\"\"\n",
    "        # 得到生成器生成的量子态\n",
    "        gen_state = self.generator()\n",
    "\n",
    "        # 判别器电路\n",
    "        state = self.discriminator(gen_state)\n",
    "        # 判别器对生成态的判断结果\n",
    "        expec_val_func = ExpecVal(Hamiltonian([[1.0, 'z2']]))\n",
    "        gen_disc_output = expec_val_func(state)\n",
    "        prob_as_target = (gen_disc_output + 1) / 2\n",
    "        \n",
    "        return prob_as_target\n",
    "\n",
    "    def forward(self, model_name):\n",
    "        if model_name == 'gen':\n",
    "            # 计算生成器的损失函数，loss值的区间为[-1, 0]，\n",
    "            # 0表示生成效果极差，为-1表示生成效果极好\n",
    "            loss =  -1 * self.disc_gen_as_target()\n",
    "        else:\n",
    "            # 计算判别器的损失函数，loss值的区间为[-1, 1]，\n",
    "            # 为-1表示完美区分，为0表示无法区分，为1表示区分颠倒\n",
    "            loss = self.disc_gen_as_target() - self.disc_target_as_target()\n",
    "\n",
    "        return loss\n",
    "\n",
    "    def get_target_state(self):\n",
    "        \"\"\"\n",
    "        得到目标态的密度矩阵表示\n",
    "        \"\"\"\n",
    "        state = partial_trace(self.target_state, 2, 4, 2)\n",
    "\n",
    "        return state.numpy()\n",
    "\n",
    "    def get_generated_state(self):\n",
    "        \"\"\"\n",
    "        得到生成态的密度矩阵表示\n",
    "        \"\"\"\n",
    "        state = self.generator()\n",
    "        state = partial_trace(state, 2, 4, 2)\n",
    "\n",
    "        return state.numpy()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "接下来我们使用 PaddlePaddle 来训练我们的模型。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "Training: 100%|#################################################| 2250/2250 [00:39<00:00, 57.65it/s]\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "the density matrix of the target state:\n",
      "[[0.02447175+0.j         0.12500004-0.09081785j]\n",
      " [0.12500004+0.09081785j 0.97552836+0.j        ]] \n",
      "\n",
      "the density matrix of the generated state:\n",
      "[[0.02444258+3.7252903e-09j 0.12302095-9.3332648e-02j]\n",
      " [0.12302095+9.3332678e-02j 0.97555757-7.4505806e-09j]] \n",
      "\n",
      "the distance between these two quantum states is 2.0483967e-05 \n",
      "\n",
      "the fidelity between these two quantum states is 0.9999947358758301\n"
     ]
    }
   ],
   "source": [
    "# 学习率\n",
    "LR = 0.1\n",
    "# 总的迭代次数\n",
    "ITR = 25\n",
    "# 每次迭代时，判别器的迭代次数\n",
    "ITR1 = 40\n",
    "# 每次迭代时，生成器的迭代次数\n",
    "ITR2 = 50\n",
    "\n",
    "# 制备目标量子态\n",
    "target_state = zero_state(num_qubits=3)\n",
    "target_state = paddle_quantum.gate.RY(0, param=0.9 * np.pi)(target_state)\n",
    "target_state = paddle_quantum.gate.RZ(0, param=0.2 * np.pi)(target_state)\n",
    "target_state.data = paddle.to_tensor(target_state.data.numpy())\n",
    "\n",
    "# 用来记录loss值的变化\n",
    "loss_history = list()\n",
    "paddle.seed(18)\n",
    "gan_demo = QGAN(target_state)\n",
    "optimizer = paddle.optimizer.SGD(learning_rate=LR, parameters=gan_demo.parameters())\n",
    "pbar = tqdm(desc=\"Training: \", total=ITR * (ITR1 + ITR2), ncols=100, ascii=True)\n",
    "for itr0 in range(ITR):\n",
    "    \n",
    "    # 记录判别器loss值的变化\n",
    "    loss_disc_history = list()\n",
    "\n",
    "    # 训练判别器\n",
    "    for itr1 in range(ITR1):\n",
    "        pbar.update(1)\n",
    "        loss_disc = gan_demo('disc')\n",
    "        loss_disc.backward()\n",
    "        optimizer.minimize(\n",
    "            loss_disc, parameters=gan_demo.discriminator.parameters(),\n",
    "            no_grad_set=gan_demo.generator.parameters()\n",
    "        )\n",
    "        gan_demo.clear_gradients()\n",
    "        loss_disc_history.append(loss_disc.numpy()[0])\n",
    "\n",
    "    # 记录生成器loss值的变化\n",
    "    loss_gen_history = list()\n",
    "\n",
    "    # 训练生成器\n",
    "    for itr2 in range(ITR2):\n",
    "        pbar.update(1)\n",
    "        loss_gen = gan_demo('gen')\n",
    "        loss_gen.backward()\n",
    "        optimizer.minimize(\n",
    "            loss_gen, parameters=gan_demo.generator.parameters(),\n",
    "            no_grad_set=gan_demo.discriminator.parameters()\n",
    "        )\n",
    "        optimizer.clear_grad()\n",
    "        loss_gen_history.append(loss_gen.numpy()[0])\n",
    "\n",
    "    loss_history.append((loss_disc_history, loss_gen_history))\n",
    "pbar.close()\n",
    "\n",
    "# 得到目标量子态\n",
    "target_state = gan_demo.get_target_state()\n",
    "\n",
    "# 得到生成器最终生成的量子态\n",
    "gen_state = gan_demo.get_generated_state()\n",
    "print(\"the density matrix of the target state:\")\n",
    "print(target_state, \"\\n\")\n",
    "print(\"the density matrix of the generated state:\")\n",
    "print(gen_state, \"\\n\")\n",
    "\n",
    "# 计算两个量子态之间的距离，\n",
    "# 这里的距离定义为 tr[(target_state-gen_state)^2]\n",
    "distance = np.trace(np.matmul(target_state-gen_state, target_state-gen_state)).real\n",
    "# 计算两个量子态的保真度\n",
    "# fidelity = state_fidelity(target_state, gen_state)\n",
    "fidelity = np.trace(\n",
    "    scipy.linalg.sqrtm(scipy.linalg.sqrtm(target_state) @ gen_state @ scipy.linalg.sqrtm(gen_state))\n",
    ").real\n",
    "print(\"the distance between these two quantum states is\", distance, \"\\n\")\n",
    "print(\"the fidelity between these two quantum states is\", fidelity)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "我们通过比较目标量子态和生成量子态的密度矩阵 $\\rho_\\text{target}$ 和 $\\rho_\\text{gen}$ 以及计算它们之间的距离 $\\text{tr}[(\\rho_\\text{target}-\\rho_\\text{gen})^2]$ 和保真度可以得知，我们的生成器生成了一个与目标态很相近的量子态。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 训练过程的可视化"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "接下来我们观察一下，在训练过程中，判别器和生成器的 loss 曲线变化过程。\n",
    "\n",
    "首先安装所需要的 package。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [],
   "source": [
    "from IPython.display import clear_output\n",
    "!pip install celluloid\n",
    "clear_output()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "接下来，我们绘制 loss 曲线的变化。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "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",
      "text/plain": [
       "<Figure size 432x288 with 2 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    }
   ],
   "source": [
    "import matplotlib.pyplot as plt\n",
    "from celluloid import Camera\n",
    "def draw_pic(loss_history):\n",
    "    fig, axes = plt.subplots(nrows=1, ncols=2)\n",
    "    camera = Camera(fig)\n",
    "    axes[0].set_title(\"discriminator\")\n",
    "    axes[0].set_xlabel(\"disc_iter\")\n",
    "    axes[0].set_ylabel(\"disc_loss\")\n",
    "    axes[0].set_xlim(0, 20)\n",
    "    axes[0].set_ylim(-1, 1)\n",
    "    axes[1].set_title(\"generator\")\n",
    "    axes[1].set_xlabel(\"gen_iter\")\n",
    "    axes[1].set_ylabel(\"gen_loss\")\n",
    "    axes[1].set_xlim(0, 50)\n",
    "    axes[1].set_ylim(-1, 0)\n",
    "    for loss in loss_history:\n",
    "        disc_data, gen_data = loss\n",
    "        disc_x_data = range(0, len(disc_data))\n",
    "        gen_x_data = range(0, len(gen_data))\n",
    "        axes[0].plot(disc_x_data, disc_data, color='red')\n",
    "        axes[1].plot(gen_x_data, gen_data, color='blue')\n",
    "        camera.snap()\n",
    "    animation = camera.animate(interval=600, \n",
    "                               repeat=True, repeat_delay=800)\n",
    "    animation.save(\"./figures/loss.gif\")\n",
    "draw_pic(loss_history)\n",
    "clear_output()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "![QGAN-fig-loss](figures/loss.gif)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "在这个动态图片中，每个帧代表一次迭代的过程。在一次迭代中，左边的红线表示判别器的 loss 曲线，右边的蓝线表示生成器的 loss 曲线。可以看出，在初始的时候，判别器和生成器每次都能从一个比较差的判别能力和生成能力逐渐学习到当前情况下比较好的判别能力和生成能力。随着学习的进行，生成器的生成能力越来越强，判别器的能力也越来越强，但是却也无法判别出真实数据和生成数据，因为这种时候生成器已经生成出了接近真实数据的生成数据，此时模型已经收敛。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "_______\n",
    "\n",
    "## 参考文献"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "[1] Goodfellow, I. J. et al. Generative Adversarial Nets. [Proc. 27th Int. Conf. Neural Inf. Process. Syst. (2014).](https://papers.nips.cc/paper/5423-generative-adversarial-nets)\n",
    "\n",
    "[2] Lloyd, S. & Weedbrook, C. Quantum Generative Adversarial Learning. [Phys. Rev. Lett. 121, 040502 (2018).](https://journals.aps.org/prl/abstract/10.1103/PhysRevLett.121.040502)\n",
    "\n",
    "[3] Benedetti, M., Grant, E., Wossnig, L. & Severini, S. Adversarial quantum circuit learning for pure state approximation. [New J. Phys. 21, (2019).](https://iopscience.iop.org/article/10.1088/1367-2630/ab14b5)"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.8.0"
  },
  "toc": {
   "base_numbering": 1,
   "nav_menu": {},
   "number_sections": true,
   "sideBar": true,
   "skip_h1_title": false,
   "title_cell": "Table of Contents",
   "title_sidebar": "Contents",
   "toc_cell": false,
   "toc_position": {},
   "toc_section_display": true,
   "toc_window_display": true
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
